#!/usr/bin/perl -w

### Notes:
# The -T for security check would help to protect the script but was giving error and was removed.
# The arguments been used as parameters on external commands/scripts. For the input file name, all "'` and ; are stripped and for the output formats, only alphanumeric _ and & are allowed.  Feel free ADD extra protectiong.
# TODO: Change time out for BM and perhaps Compressor


use XML::XPath;
use POSIX 'strftime';
use POSIX 'setsid';
use URI::Escape;
use URI;
use Data::Validate::URI qw(is_uri);
use LWP::UserAgent;
use Fcntl qw(:DEFAULT :flock :seek);

use FCTranscode;


### Subroutines declarations ###

sub col_error_die{

	my $remote_id = shift;
	my $new_status = shift;

	write_log("Error die for $remote_id: $new_status");
	sysopen(FH, $collector_file, O_RDWR | O_CREAT);
	flock FH, LOCK_EX;
        @lines = <FH>;
	seek(FH,0,SEEK_SET);
	truncate FH, 0;
        foreach my $line (@lines) {
        	chomp($line);
	        #What is it doing here? Does it need an else?
        	if ($line =~ /remote_id=\'$remote_id\'/) {
			$line =~ s/status=\'[^\']+\'/status=\'Failed:$new_status\'/;
		}
		print FH $line."\n";
        }
	flock FH, LOCK_UN;
        close FH;
	error_die($new_status);
}

sub col_quick_warn{

        my $remote_id = shift;
        my $new_status = shift;

        write_log("Quick warn for $remote_id: $new_status");
        sysopen(FH, $collector_file, O_RDWR | O_CREAT);
        flock FH, LOCK_EX;
        @lines = <FH>;
        seek(FH,0,SEEK_SET);
        truncate FH, 0;
        foreach my $line (@lines) {
                chomp($line);
                #What is it doing here? Does it need an else?
                if ($line =~ /remote_id=\'$remote_id\'/) {
			#write_log("Match for $remote_id the line is $line");
                        $line =~ s/status=\'[^\']+\'/status=\'Failed:$new_status\'/;
                }
                print FH $line."\n";
        }
        flock FH, LOCK_UN;
        close FH;
        quick_warn($new_status);
}



sub download_file{
	my $file_url = shift;
	my $file_location = shift;

	my $ua = LWP::UserAgent->new;
	$ua->timeout(10);
	$ua->env_proxy;

	my $response = $ua->get($file_url, ':content_file' => $file_location); # Use :content_file to save straight to a file
	if ($response->is_success){
		$response->decoded_content;
		write_log ("Downloaded $file_url");
        	# Check for error with defined
        	#open(FH, ">", $file_location);
		#flock FH, LOCK_EX;
	        #print FH $file_content;
		#flock FH, LOCK_UN;
	        #close(FH);
	}
	else {
		write_log("Failed to download: $file_location\n".$response->status_line);
		exit;
	}
}

sub col_error_die_all{
        my @out_formats = shift;
	my $message = shift;
        foreach my $out_format (@out_formats) {
                next if ($out_format eq "");
                write_log("General error in conv.pl reverting collector status file.");
                $out_format = uri_unescape($out_format);

                # Should I check to see if $out_format is of the form something:something?

                @parts=split(/:/, $out_format);
                $out_setting = $parts[0];
                $remote_id = $parts[1];
                col_quick_warn($remote_id, $message);
		exit(1);
        }
}

sub remove_new_items{
	my @out_formats = shift;
	foreach my $out_format (@out_formats) {
		next if ($out_format eq "");
		write_log("General error in conv.pl reverting collector status file.");
                $out_format = uri_unescape($out_format);

                # Should I check to see if $out_format is of the form something:something?

                @parts=split(/:/, $out_format);
                $out_setting = $parts[0];
                $remote_id = $parts[1];
		remove_remote_id($remote_id);
	}
}

### Start og main program ###

# Using to calculate duration which is used to select the dvd quality 
$hours = 0;
$mins = 0;
$secs = 0;

# TODO - Process command line argument arguments 
#if ($#ARGV!=1) {
#	# Usage: $0 input_file out_formats=f1[&out_formats=f2[&out_f...]]
#	error_die("ERROR:3001: Wrong number of parameters for $0.");
#	exit 0;
#}

$input_file = uri_unescape($ARGV[0]);

# Filtering arguments and building format list (if changing the pattern please do the same on the calling program(ServiceInterface.java))
# Need to create a filter for ARGV[0]
$input_file =~ s/[;"'\`\\\|]//g;

write_log("Transcoding $input_file");

$ARGV[1] =~ s/[^\w|&|\:|\.|-|%]//g;     # TODO: same as above. Maybe we don't need & as it will be striped. Will a \& became a & ?
@out_formats = split("&",$ARGV[1]);
write_log("For formats: $ARGV[1]");

if (is_uri($input_file)) {
	write_log("It's an URL");

	$file_name = URI->new($input_file)->path;
	$file_name =~ /([^\/]+)$/;
	$file_name = $1;
	#$file_name =~ /(.*)\.\w+$/; # Could be merged with the regex above
	#$file_name = $1;

	if($1 eq "") {
		write_log("Couldn't extract a filename!");
		#TODO - Deal with error
		exit;
	}
	else {
		$file_name = $1;
	}
	
	chdir '/'                       or error_die("ERROR:3005: Could not \'chdir /\'\n");
        defined($pid = fork)            or error_die("ERROR:3006: Could not fork\n");
        if ($pid) {
		# The parent
		sleep(1);
		print "OK: DOWNLOADING\n";
		exit;
	}
	else {
		open STDIN, '/dev/null'         or col_error_die("ERROR:3008: Could not open /dev/null as STDIN on child.");
        	open STDOUT, '>/dev/null'       or col_error_die("ERROR:3009: Could not open /dev/null as STDOUT on child.");
        	setsid;
        	open STDERR, '>&STDOUT'         or col_error_die("ERROR:3010: Could not redirect STDERR to STDOUT on child.");

		foreach my $out_format (@out_formats){
			next if ($out_format eq "");
			write_log("Creating collector for $out_format");
			$out_format = uri_unescape($out_format);

        		# Should I check to see if $out_format is of the form something:something?

		        @parts=split(/:/, $out_format);
        		$out_setting = $parts[0];
	        	$remote_id = $parts[1];
			$col_element_com = "<job remote_id=\'$remote_id\' jobid='n/a' batchid='$batchid_ret' transcoder='n/a' col_status='downloading'";
                	$col_element_com .= " percentComplete='0' timeElapsed='n/a' timeRemaining='n/a' format='$out_setting' input='$input_file' output='n/a' status='downloading' />";
                	system("/bin/echo \"$col_element_com\" >> \"$collector_file\"");
		}
		download_file($input_file, $cache_dir.$file_name);
		$input_file = $cache_dir.$file_name;
	}
}
else {
	write_log("Not a URL");
	col_error_die_all("ERROR:3000:Not an URL. Only URLs are supported");
}

# Filtering arguments and building format list (if changing the pattern please do the same on the calling program(ServiceInterface.java))
# Need to create a filter for ARGV[0]
$input_file =~ s/[;"'\`\\\|]//g;	# Hope that filtering \ won't make problems like.. dir\ name... TODO: add & and maybe others

# TODO - Check for existance of input file

# Get media duration (maybe put a few checks/warnings?)
unless (-e $mediainfo) {
	col_error_die_all("ERROR:3002: mediainfo not found.");
}
$raw_minfo = `$mediainfo --Output=XML \'$input_file\'`;
$xml_content = XML::XPath->new($raw_minfo)	or col_error_die_all("ERROR:3003: Could not instantiate XPath.");
# Not sure if the bellow should be an error or warning. Need to match the quickwarn and ERROR:
$duration_raw = $xml_content->getNodeText('/Mediainfo/File/Video/Duration') or quick_warn("ERROR:3004: Invalid node path for mediainfo XML.");

# Looks like then warnings comes from $1 been undefined. TODO - Check $1 before assignment 
$duration_raw =~ /(\d+)h/;
$hours = $1;
$duration_raw =~ /(\d+)mn/;
$mins = $1;
$duration_raw =~ /(\d+)s/;
$secs = $1;

$totalsecs = ((($hours*60)+$mins)*60)+$secs+1;

# Extract media file name (without extension) 
$input_file =~ /([\w\ ]+)\..*/;
$output_name= $1;

unless (-e $input_file) { col_error_die_all("ERROR:1001: Input file \"$input_file\" does not exist."); } 

# For each selected format
foreach my $out_format (@out_formats){
	next if ($out_format eq "");

	$out_format = uri_unescape($out_format);

	# Should I check to see if $out_format is of the form something:something?

	@parts=split(/:/, $out_format);
        $out_setting = $parts[0];
        $remote_id = $parts[1];

	unless ((-e $settingsdir.$out_setting.".setting") or ($out_setting eq "flash_lb")) { col_quick_warn($remote_id, "ERROR:2001: Setting file $settingsdir$out_setting.setting does not exist."); next; }

	# If Flash Video use ffmpeg 
	if ($out_setting eq "flash_lb") {

		$t_curr = time;

		# Run ffmpeg
		$ffmpeg_status_file = "$ffmpeg_status_dir$output_name.$out_setting.$t_curr.status";
		#TODO - Use -X to check if file is writable - open should give an error but this will happen after forking

		unless(chdir '/') {col_quick_warn($remote_id,"ERROR:3005: Could not \'chdir /\'\n"); next; }	
		unless(defined($pid = fork)) {col_quick_warn($remote_id,"ERROR:3006: Could not fork\n"); next; }	
		if ($pid) {
                	sleep 2;
                	$check = `tr '\\r' '\\n' < \"$ffmpeg_status_file\" | tail -1`;		#TODO check for errors here.
                	#print "$check\n";
			if ($check =~ /\Aframe.+time.+bitrate.+/){

				# Take Note: collector status file uses ' and not "
                        	$col_element_ffm = "<job remote_id=\'$remote_id\' jobid='$ffmpeg_status_file' batchid='n/a' transcoder='ffmpeg' col_status='waiting'";
				$col_element_ffm .= " percentComplete='0' timeElapsed='n/a' timeRemaining='n/a' format='$out_setting' input='$input_file' output='".uri_escape("$storage_url$output_name.$out_setting.flv")."' status='Started' />";

				sysopen(FH, $collector_file, O_RDWR | O_CREAT); # Check for failure
				flock FH, LOCK_EX;
	                        @lines = <FH>;
				seek(FH,0,SEEK_SET);
				truncate FH, 0;
        	                foreach $line (@lines) {
	                                chomp($line);
                                	#What is it doing here? Does it need an else?
                        	        if ($line =~ /remote_id=\'$remote_id\'/) { write_log($col_element_ffm); print FH $col_element_ffm."\n"; }
                	                else { write_log($line); print FH $line."\n"; }
        	                }
				flock FH, LOCK_UN;
	                        close FH;

                #        	system("/bin/echo \"$col_element_ffm\" >> \"$collector_file\""); #TODO - Check for failures

		#		print "OK: $remote_id\n";	# Transcoding successfully started.
		#		print "$ffmpeg_status_file\n";		# jobid (ffmpeg_status_file)
		#		print "$outputdir$output_name.$out_setting.flv";		# output file TODO - Check if it's writable
			}
				
			else {
				write_log($check);
				col_error_die($remote_id,"ERROR:3007: Could not confirm start of transcoding.\n"); # Get a better condition and reason
			}
			next;
		}
		else {
			open STDIN, '/dev/null'		or col_quick_warn($remote_id, "ERROR:3008: Could not open /dev/null as STDIN on child.");
			open STDOUT, '>/dev/null'	or col_quick_warn($remote_id, "ERROR:3009: Could not open /dev/null as STDOUT on child.");
			setsid;
			open STDERR, '>&STDOUT'		or col_quick_warn($remote_id, "ERROR:3010: Could not redirect STDERR to STDOUT on child.");

			# Write child's pid , current epoch time and output file name into ffmpeg status file
			system("/bin/echo \"$$\n$t_curr\n$output_name.$out_setting.flv\" > \"$ffmpeg_status_file\""); #TODO - Check for failure / Infact check all system()s / $! should have the command
			$cmd_str = "ffmpeg -deinterlace -y -i \"$input_file\" \"$outputdir$output_name.$out_setting.flv\" 2>> \"$ffmpeg_status_file\"";
			write_log($cmd_str);
			system($cmd_str);	# TODO - Check for failure
			$t_fin = time;
			$t_ela = $t_fin-$t_curr;
			@tEla_parts = gmtime($t_ela);
			$t_ela_str = sprintf("%d days, %02d:%02d:%02d",@tEla_parts[7,2,1,0]);
			if ($? == 0) {
				system("/bin/echo \"FINISH: $t_fin\" >> \"$ffmpeg_status_file\"");	#TODO - check for failure
				write_log("ffmpeg finished!");
				$col_element_ffm  = "<job remote_id=\'$remote_id\' jobid='$ffmpeg_status_file' batchid='n/a' transcoder='ffmpeg' col_status='finished' ";
                        	$col_element_ffm .= "percentComplete='100' timeElapsed='$t_ela_str' timeRemaining='0' format='$out_setting' input='$input_file' status='Finished' output='".uri_escape("$storage_url$output_name.$out_setting.flv")."' />";
				sysopen(FH, $collector_file, O_RDWR | O_CREAT); # TODO - Check for failure
				flock FH, LOCK_EX;
				@lines = <FH>;
				seek(FH,0,SEEK_SET);
				truncate FH, 0;
				foreach $line (@lines){
					chomp($line);
					# Add a next at the end of the if block?
					if ($line =~ /jobid=\'$ffmpeg_status_file\'/) { print FH $col_element_ffm."\n"; }
					else { print FH $line."\n"; }
				}
				flock FH, LOCK_UN;
				close FH;
				exit(0);
			}
			elsif ($? == -1) {
				col_error_die($remote_id, "ERROR:3011: Could not execute $!.");
			}
			elsif ($ff_sig = (($? >> 8) - 128)) {
				
				system("/bin/echo \"CANCEL: $t_fin\" >> \"$ffmpeg_status_file\"");	# TODO - Check for failure
				system("rm -rf \"$outputdir$output_name.$out_setting.flv\"");	# TODO - Check for failure
				write_log("Job canceled with signal $ff_sig.");
			}
		}
	}
	
	# Else use Compressor
	else {
		if ($out_setting eq "dvd_bq" || $out_setting eq "dvd_fe") {
			if     ($totalsecs <= 90) { $out_setting.='_90'; }
			elsif ($totalsecs <= 120) { $out_setting.='_120'; }	
			elsif ($totalsecs <= 150) { $out_setting.='_150'; }
			else   { $out_setting.='_150'; print quick_warn("WARN:6001: Video too long for a DVD. Using 150 mins setting.");}
		}
	
		if ($out_setting eq "hd_dvd") {
                	if     ($totalsecs <= 30) { $out_setting.='_mpeg2_30'; }
	                elsif ($totalsecs <= 60) { $out_setting.='_h264_60'; }
        	        elsif ($totalsecs <= 90) { $out_setting.='_h264_90'; }
                	else   { $out_setting.='_90'; print quick_warn("WARN:6002: Video too long for a HD DVD. Using 90 mins setting.");}
		}

		# Read the setting file
		open(my $setting, "<", "$settingsdir$out_setting.setting") or col_error_die($remote_id, "ERROR:3012: Could not open $settingsdir$out_setting.setting: $!.");
		my $setting_content = <$setting>;
		close $setting; # Should I put warnings in close 'file'?    or quick_warn("WARN:8001: Could not close $setting: $!.");
	
		# Extract extension for output file
		$setting_content =~ /<file-extension>(\w+)<\/file-extension>/;	# TODO - check for mismatch
		my $out_ext = $1;

		# Run Compressor
		# 2>&1 redirects the stderr to stdout since the job/batch info comes from stderr
		my $cmd_str = "/Applications/Compressor.app/Contents/MacOS/Compressor -clustername \"This Computer\" -jobpath \"$input_file\" -settingpath \"$settingsdir$out_setting.setting\" -destinationpath \"$outputdir$output_name.$out_setting.$out_ext\" 2>&1";
		#write_log($cmd_str);
		#print $cmd_str."\n";
		$output = `$cmd_str`;	#TODO - Check for errors?
		#write_log("Compressor output:\n$output");

		if ($output =~ /<jobID ([[:xdigit:]-]{36}) \/>/) {
			#write_log("jobid match for: $1");
			$jobid_ret = $1; }
		else {
			#write_log("no jobid match $1");
			col_quick_warn($remote_id, "Could not start transcoding for $out_setting. Compressor returned no job id."); }


                if ($output =~ /<batchID ([[:xdigit:]-]{36}) \/>/) {
			#write_log("jobid match for:$1");
			$batchid_ret = $1; }
		else {
			#write_log("no batchid match $1");
			col_quick_warn($remote_id, "Could not start transcoding for $out_setting. Compressor returned no batch id."); }

		#write_log(" Jobid: $jobid_ret    Batchid: $batchid_ret");

		chdir '/'                       or col_error_die($remote_id, "ERROR:3005: Could not \'chdir /\'\n");
                defined($pid = fork)            or col_error_die($remote_id, "ERROR:3006: Could not fork\n");
                if ($pid) {

			# Take Note: collector status file uses ' and not "
			$col_element_com = "<job remote_id=\'$remote_id\' jobid='$jobid_ret' batchid='$batchid_ret' transcoder='compressor' col_status='waiting'";
                        $col_element_com .= " percentComplete='0' timeElapsed='n/a' timeRemaining='n/a' format='$out_setting' input='$input_file' output='".uri_escape("$storage_url$output_name.$out_setting.$out_ext")."' status='transcoding' />";

			sysopen(FH, $collector_file, O_RDWR | O_CREAT); # Check for failure
			flock FH, LOCK_EX;
                        @lines = <FH>;
			seek(FH,0,SEEK_SET);
			truncate FH, 0;
                        foreach $line (@lines) {
                                chomp($line);
                                #What is it doing here? Does it need an else?
                                if ($line =~ /remote_id=\'$remote_id\'/) {
					#write_log($col_element_com);
					print FH $col_element_com."\n"; }
                                else {
					#write_log($line);
					print FH $line."\n"; }
                        }
			flock FH, LOCK_UN;
                        close FH;


		#	system("/bin/echo \"$col_element_com\" >> \"$collector_file\"");
		#	print "OK: $remote_id\n";
		#	print $jobid_ret."\n";	#jobid
		#	print "$outputdir$output_name.$out_setting.$out_ext\n";	#outputfile
			next; #I think it should be a next and not a exit;
		}
		# I don't think I need to loop! We run Batch monitor when we need!
		else {
			sleep 2;

			open STDIN, '/dev/null'         or col_error_die($remote_id, "ERROR:3008: Could not open /dev/null as STDIN on child.");
                        open STDOUT, '>/dev/null'       or col_error_die($remote_id, "ERROR:3009: Could not open /dev/null as STDOUT on child.");
                        setsid;
                        open STDERR, '>&STDOUT'         or error_die($remote_id, "ERROR:3010: Could not redirect STDERR to STDOUT on child.");

			$cmd_str = "/Applications/Utilities/Batch\\ Monitor.app/Contents/MacOS/Batch\\ Monitor -clustername \"This Computer\" -jobid $jobid_ret -batchid $batchid_ret";

			do {
				$output = `$cmd_str`; # Is there a way to find errors for backticks? Perhaps checking output for ""?

				#TODO - Check for BM's Timed out
				open(FH, "<", \$output);
				@lines = <FH>;

				# Is there a way to find out if the last =~ got any match? so we can check before assignment
				foreach $line (@lines){
					if ($line =~ /\A<batchStatus/) { next; }
				        elsif ($line =~ /\A<jobStatus/){

			        	        $line =~ /jobid=\"([[:xdigit:]-]{36})/;
        			        	$jobid = $1;

        	                                if ($jobid ne $jobid_ret) {next;}

						$line =~ /percentComplete=\"(\d{1,3})\"/;
                                                $percomp = $1;

                                                if ($percomp ne "100") {next;}

			                	$line =~ /batchid=\"([[:xdigit:]-]{36})/;
	        			        $batchid = $1;

		        		        $line =~ /status=\"([^\"]+)\"/;
                				$status = $1;

		                		$line =~ /timeRemaining=\"([\w\(\)\ ]+)\"/;
	        		        	$timerem = $1;

			        	        $line =~ /timeElapsed=\"([\w\(\)\ ]+)\"/;
			                	$timeelap = $1;

						last;
					}
					else {
						write_log("ATTENTION new type of line returned by Batch Monitor. Inform developers:\n$line");
						}
				}
				close FH;
				sleep 30;

			} while ($percomp ne "100");

			if ($status eq 'Successful')	{ $col_status='finished';}
			elsif ($status eq 'Cancelled')	{ $col_status='cancelled'; }
			elsif ($status =~ /^Failed/)	{ $col_status='failed'; 	$status="ERROR:0000:$status"; }
			else				{ $col_status='failed';		write_log("Illegal status!(Inform developers): $status"); }

			write_log("Compressor finished\n");
                        $col_element_com  = "<job remote_id=\'$remote_id\' jobid='$jobid' batchid='$batchid' transcoder='compressor' col_status='$col_status' ";
                        $col_element_com .= "percentComplete='$percomp' timeElapsed='$timeelap' timeRemaining='$timerem' format='$out_setting' input='$input_file' status='$status' output='".uri_escape("$storage_url$output_name.$out_setting.$out_ext")."' />";

			sysopen(FH, $collector_file, O_RDWR | O_CREAT); # Check for failure
			flock FH, LOCK_EX;
			@lines = <FH>;
			seek(FH,0,SEEK_SET);
			truncate FH, 0;
			foreach $line (@lines) {
				chomp($line);
				#What is it doing here? Does it need an else?
				if ($line =~ /jobid=\'$jobid\'/) { write_log($col_element_com); print FH $col_element_com."\n"; }
				else { write_log($line); print FH $line."\n"; }
			}
			flock FH, LOCK_UN;
			close FH;
			exit;

		}
	}
}
